查看原文
其他

音视频基础能力之 Android 音频篇:高性能音频采集

声知视界 郭霖
2024-12-27



/   今日科技快讯   /


据悉,久未公开露面的马云也在当天晚间现身蚂蚁园区。马云与蚂蚁员工分享了自己对新科技、新趋势的思考:“AI会改变一切,但这不代表,AI能决定一切。技术固然重要,但是未来真正决定胜负的,还是今天我们为这个即将到来的时代做些什么真正有价值而又是与众不同的事。”


/   作者简介   /


本篇文章来自声知视界的投稿,文章主要分享了高性能音频采集,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


声知视界的博客地址:

https://juejin.cn/user/1432042970031882/posts


/   前言   /


涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能。

本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。


本文为该系列文章的第 3 篇,也是有关音频采集的最后一篇,将详细讲述在 Android 平台如何实现高性能音频采集。在之前的文章里面,我们详细的介绍了使用 Java 相关的 API 来实现音频的采集和录制。但是在低延迟音视频或者跨平台的项目中,还是会优先考虑 Android 平台提供的 c/c++ 接口,因为不仅可以提升程序性能,还能最大限度上来缩短音频延迟。


Android 平台提供了三种 c/c++ 接口来实现音频采集,分别是:


  • Opensl es 嵌入式、跨平台的免费音频处理库,为嵌入式设备提供标准化、高性能、低延迟的 API。NDK 中包含的 Opensl es 1.0.1 API 是 Khronos Group为 Android 平台量身打造的一个版本。

  • AAudio  Android O(Android 8) 版本中引入的全新的 Android C API,此 API 专为需要低延迟的高性能音频应用而设计。

  • Oboe Android 团队打造的一个 c++ 库,可以在 android 上构建高性能应用应用,它的主要目的是让开发者能够使用简化的 API,该 API 最低可以在 Android API 16 (Jelly Bean) 上运行。


你可能比较好奇,为什么给开发者提供这么多的 c/c++ 的音频采集接口出来?其实还是有点原因的,简单的来说,使用 Opensl es 的接口来实现音频采集比较繁琐,实现起来要写大几百行代码(可以参考下文章底部的 sample code 链接)。


所以,Android 团队又设计了一套 AAudio 的接口给开发者使用,但是 AAudio 又无法在 Android 8 以下的设备上运行,然后又搞了一套 Oboe 库,它内部自动帮你完成回退,Android 8 以下使用 opensl es接口,Android 8 以上使用 AAudio 库,就这么回事。


下面,详细介绍下三种库的接入和使用姿势。如果不想听笔者废话,想直接看代码的话,可以直接跳转到文章底部。


/   申请权限   /


任何使用到音频采集的 app 都需要申请音频录制权限,无论开发者使用的是 Java 接口还是 c/c++ 接口。配置方式,Android 项目的清单文件中添加:


<uses-permission android:name="android.permission.RECORD_AUDIO"/>

在 API 23 (Android 6.0) 之后,为了保护用户隐私,对于一些敏感权限(比如录音权限),应用需要在运行时动态申请。示例代码如下:


private static final int PERMISSION_REQUEST_CODE = 1;

// step1: 检查是否有录音权限
private boolean checkPermission() {
    int result = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
    return result == PackageManager.PERMISSION_GRANTED;
}

// step2: 请求录音权限
private void requestPermission() {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, PERMISSION_REQUEST_CODE);
}

// step3: 处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == PERMISSION_REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限被授予,可以进行录音
            startRecording(); 
        } else {
            // 权限被拒绝,无法进行录音
            Toast.makeText(this, "录音权限被拒绝", Toast.LENGTH_SHORT).show();
        }
    }
}


/   Opensl es   /


上一章节曾说过,通过 Opensl es 来实现音频采集会比较繁琐,繁琐的原因是因为 Opensl es 能做的事情比较多,所以在接口层面上设计会更优先考虑到拓展性和模块化。所以,在接入 Opensl es 之前还是有必要先了解下它的编程模型。


Opensl es 使用 c 接口实现了一套类似于 Java/c++ 面向对象的概念,所有对象的初始接口叫做 SLObjectItf ,它的声明以及简单实用如下:


struct SLObjectItf_ {

 SLresult (*Realize) (
  SLObjectItf self,
  SLboolean async
 );

 SLresult (*GetInterface) (
  SLObjectItf self,
  const SLInterfaceID iid,
  void * pInterface
 );
 
 //... 此处省略其他接口

};

//1. 创建对象
SLObjectItf sl_object_;
SLresult result = slCreateEngine(&sl_object, args...);

//2. 示例化
(*sl_object)->Realize(sl_object, args..);


首先通过初始接口创建出这个对象,然后实现 (realize),这个和我们常见的编程模式差不多。唯一的区别是,你不能通过这个对象来调用其内部的函数,而是调用它内部的函数指针,将其句柄传递进去。



SLObjectItf 对象的生命周期时序图如上,下面开始讲解下如何在项目中集成和使用。


项目集成


引入头文件:


#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h> //Android相关拓展


引入库文件:


target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        OpenSLES)


调用流程


初始化流程


step1: 首先通过 SLObjectItf 创建出 SLEngineItf 引擎对象 sl_engine_ 出来。


// Create the engine object in thread safe mode.
const SLEngineOption option[] = {
        {SL_ENGINEOPTION_THREADSAFE, static_cast<SLuint32>(SL_BOOLEAN_TRUE)}};
SLresult result = slCreateEngine(&sl_object_, 0, option, 0, NULL, NULL);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("slCreateEngine failed: %s", GetSLErrorString(result));
  return SV_CRATE_ERROR;
}

result = (*sl_object_)->Realize(sl_object_, SL_BOOLEAN_FALSE);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("sl_object Realize failed: %s", GetSLErrorString(result));
  return SV_CRATE_ERROR;
}

result = (*sl_object_)->GetInterface(sl_object_, SL_IID_ENGINE, &sl_engine_);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("sl_object SL_IID_ENGINE get Interface failed: %s", GetSLErrorString(result));
  return SV_CRATE_ERROR;
}


step2: 通过 SLEngineItf 引擎对象创建 SLObjectItf 录制模块对象 sl_record_obj_ 出来。


// 1. configure audio source
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE,
                                  SL_IODEVICE_AUDIOINPUT,
                                  SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};

// 2. configure audio sink
SLDataLocator_AndroidSimpleBufferQueue buffer_queue = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, SV_OPENSLES_BUFFERS_LEN};
SLDataFormat_PCM format_pcm = {
        SL_DATAFORMAT_PCM,           static_cast<SLuint32>(channel),
        GetSamplePerSec(sample_rate),          SL_PCMSAMPLEFORMAT_FIXED_16,
        SL_PCMSAMPLEFORMAT_FIXED_16, GetChannelMask(channel),
        SL_BYTEORDER_LITTLEENDIAN};
SLDataSink audioSink = {&buffer_queue, &format_pcm};

// 3. create audio recorder
// (requires the RECORD_AUDIO permission)
const SLInterfaceID id[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                            SL_IID_ANDROIDCONFIGURATION};
const SLboolean req[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};

SLresult result = (*sl_engine_)->CreateAudioRecorder(sl_engine_, &sl_record_obj_, &audioSrc,
                              &audioSink, arraysize(id), id, req);

if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Create AudioRecorder failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}


step3: 配置音频参数并实例化 sl_record_obj_ 。


//4. Configure the audio recorder. before it it realized.
SLAndroidConfigurationItf recorder_config;
result = (*sl_record_obj_)->GetInterface(sl_record_obj_, SL_IID_ANDROIDCONFIGURATION, &recorder_config);
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Get record configuration failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}

//5. set audio source.
SLint32 stream_type = SL_ANDROID_RECORDING_PRESET_GENERIC; // AudioSource.DEFAULT
result = (*recorder_config)->SetConfiguration(recorder_config, SL_ANDROID_KEY_RECORDING_PRESET, &stream_type, sizeof(SLint32));
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Set recorder preset failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}

//6. set audio performance.
SLuint32 performance_mode = SL_ANDROID_PERFORMANCE_LATENCY;
SLuint32  value_size = sizeof(SLuint32);
result = (*recorder_config)->GetConfiguration(recorder_config, SL_ANDROID_KEY_PERFORMANCE_MODE, &value_size, &performance_mode);
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("Set recorder performance mode failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}

//7. Realize record object.
result = (*sl_record_obj_)->Realize(sl_record_obj_, SL_BOOLEAN_FALSE);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("sl_record_obj Realize failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}


step4: 获取 SLRecordItf 采集对象 sl_record_  和  BufferQueue (SLAndroidSimpleBufferQueueItf)。


//8. Get record interface.
result = (*sl_record_obj_)->GetInterface(sl_record_obj_, SL_IID_RECORD, &sl_record_);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("sl_record_obj GetInterface SL_IID_RECORD failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;
}

//9. Get BufferQueue.
result = (*sl_record_obj_)->GetInterface(sl_record_obj_, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &record_buffer_queue_);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("sl_record_obj GetInterface SL_IID_ANDROIDSIMPLEBUFFERQUEUE failed: %s",
          GetSLErrorString(result));
  return SV_INIT_ERROR;
}

//10. Register buffer queue.
result = (*record_buffer_queue_)->RegisterCallback(record_buffer_queue_, BufferQueueCallBack, this);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("record_buffer_queue RegisterCallback failed: %s", GetSLErrorString(result));
  return SV_INIT_ERROR;}


开启音频采集


调用逻辑比较简单,需要注意的是,Opensl es 开启采集的触发开关是,需要调用一次 BufferQeue 的 Enqueue 函数,否则之前注册的回调接口是不会吐帧出来的。


//1. 首先要判断下状态是否正确,相关的对象是否已完成初始化。如果没有,则返回错误。
...

//2.重置状态,并清空缓冲区
SLresult result = (*sl_record_)->SetRecordState(sl_record_, SL_RECORDSTATE_STOPPED);
if (result != SL_RESULT_SUCCESS) {
  return SV_START_RECORDING_ERROR;
}

SLAndroidSimpleBufferQueueState state;
result = (*record_buffer_queue_)->GetState(record_buffer_queue_, &state);
if (result != SL_RESULT_SUCCESS) {
  AV_LOGW("StartRecording GetState failed: %s", GetSLErrorString(result));
  return SV_START_RECORDING_ERROR;
}

auto buffer_count_in_queue = state.count;
if (buffer_count_in_queue > 0) {
  (*record_buffer_queue_)->Clear(record_buffer_queue_);
}

//3. 向缓冲队列请求音频帧
for (int i = 0; i < SV_OPENSLES_BUFFERS_LEN - buffer_count_in_queue; i++) {
  auto audio_buffer = reinterpret_cast<SLint8 *>(audio_buffers_[0].get());
  auto len = buffer_len_ * 16 / 8;
  SLresult err = (*record_buffer_queue_)->Enqueue(record_buffer_queue_, audio_buffer, len);
  if (err != SL_RESULT_SUCCESS) {
    AV_LOGW("Enqueue failed, err: %s", GetSLErrorString(err));
    return SV_START_RECORDING_ERROR;
  }
}

//4. 设置当前为采集状态
result = (*sl_record_)->SetRecordState(sl_record_, SL_RECORDSTATE_RECORDING);


接下来,注册至 bufferqueue 的回调函数会一直吐帧,根据自己的业务去完成相应的逻辑即可,这里就演示下简单的写文件操作。


//1. 判断状态是否正确
SLuint32 state;
SLresult result = (*sl_record_)->GetRecordState(sl_record_, &state);
if(SL_RESULT_SUCCESS != result) {
  AV_LOGW("GetRecordState failed, err: %s", GetSLErrorString(result));
  return;
}

if(state != SL_RECORDSTATE_RECORDING) {
  AV_LOGW("Buffer callback in non-recording state! state: %d", state);
  return;
}

//2. 从bufferqueue拿出音频帧
auto audio_buffer = reinterpret_cast<SLint8 *>(audio_buffers_[0].get());
auto len = buffer_len_ * 16 / 8;
AV_LOGI("audio buffer len: %u", len);

result = (*record_buffer_queue_)->Enqueue(record_buffer_queue_, audio_buffer, len);
if(SL_RESULT_SUCCESS != result) {
  AV_LOGW("Enqueue failed: err: %s", GetSLErrorString(result));
  return;
}

//3. 写文件
if(file_) {
  size_t write_len = fwrite(audio_buffer, 1, len, file_);
  AV_LOGW("write into file size: %d", write_len);
}


停止采集并销毁


设置采集状态为 STOPPED 状态,并清空 BufferQueue。理论上这步执行完成之后,回调函数将停止吐帧。


SLresult result = (*sl_record_)->SetRecordState(sl_record_, SL_RECORDSTATE_STOPPED);
if(result != SL_RESULT_SUCCESS) {
  AV_LOGW("StopRecording SetRecordState failed.");
  return SV_STOP_ERROR;
}
(*record_buffer_queue_)->Clear(record_buffer_queue_);


最后,释放相关的资源。


(*record_buffer_queue_)->RegisterCallback(record_buffer_queue_, nullptr, nullptr);
(*sl_record_obj_)->Destroy(sl_record_obj_);
(*sl_object_).Destroy(sl_object_);
//...


/   AAudio   /


AAudio 是在 Android O 版本引入的全新 C API,此 API 专为需要低延迟的高性能音频应用设计的。再学习使用它之前,笔者认为有必要先阅读下 AAudio 运行时的状态,这对理解后续的代码还是有好处的。



上图是官网给出的 AAudio 的运行时状态图,其中稳定的状态共有五种,分别是:


  • 打开

  • 已开始

  • 已暂停

  • 已刷新

  • 已停止


当状态切换至 “已开始”,音频帧才会吐给应用程序这边。需要注意的是,调用切换状态的 API 都是异步的,例如图中的 requestPause, requestStop等。AAudio 并没有提供回调函数通知你进入下一状态,如果你有同步的相关需求,可以通过 AAudioStream_waitForStateChange 来等待状态变更。


//只会等待你设定的状态
AAUDIO_API aaudio_result_t AAudioStream_waitForStateChange(
  AAudioStream *_Nonnull stream,
  aaudio_stream_state_t inputState,
  aaudio_stream_state_t *_Nullable nextState,
  int64_t timeoutNanoseconds
)


项目集成


引入头文件:


#include <aaudio/AAudio.h>

引入库文件:


target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        aaudio)


调用流程


初始化流程


step1: audio config


AAudioStreamBuilder_setDeviceId(builder_, AAUDIO_UNSPECIFIED);
AAudioStreamBuilder_setSampleRate(builder_, sample_rate);
AAudioStreamBuilder_setChannelCount(builder_, channel);
//音频样本数,一般填 16bit,越高音质越好。
AAudioStreamBuilder_setFormat(builder_, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setSharingMode(builder_, AAUDIO_SHARING_MODE_SHARED);
//input 表示采集
AAudioStreamBuilder_setDirection(builder_, AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setPerformanceMode(builder_, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDataCallback(builder_, AVDataCallback, this);
AAudioStreamBuilder_setErrorCallback(builder_, AVErrorCallback, this);


有几个重要的参数:


  • DeviceId: 一般都不用自己指定,填 AAUDIO_UNSPECIFIED 就行。如果有特殊需求,通过Java AudioManager.getDevices() 来获取音频设备的 ID。

  • SharingMode:  共享模式

    • AAUDIO_SHARING_MODE_EXCLUSIVE 表示对音频设备进行独占访问。

    • AAUDIO_SHARING_MODE_SHARED 表示允许 AAudio 混合音频,默认为共享。

  • PerformanceMode:

    • AAUDIO_PERFORMANCE_MODE_NONE 默认模式,这种模式使用在延迟时间和节能之间取得平衡。

    • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 使用较小的缓冲区和经优化的数据路径,以减少延迟时间。

    • AAUDIO_PERFORMANCE_MODE_POWER_SAVING 使用较大的内部缓冲区,以及以延迟时间为代价换取节能优势的数据路径。


step2: open stream


//step2: open stream.
auto result = AAudioStreamBuilder_openStream(builder_, &stream_);
if (result != AAUDIO_OK) {
  AV_LOGW("InitRecording error: %d, reason: %s", result, AAudio_convertResultToText(result));
  return SV_INIT_ERROR;
}


开启音频采集


先检查当前流的状态是否是 “打开” 状态,如果是,则请求开始。


aaudio_stream_state_t stream_state =  AAudioStream_getState(stream_);
if (stream_state != AAUDIO_STREAM_STATE_OPEN) {
  AV_LOGW("StartRecording error,  Invalid state.");
  return SV_START_RECORDING_ERROR;
}

aaudio_result_t result = AAudioStream_requestStart(stream_);
if (result != AAUDIO_OK) {
  AV_LOGW("StartRecording error:%d, reason:%s", result, AAudio_convertResultToText(result));
  return SV_START_RECORDING_ERROR;
}


请求完成之后,之前设置的回调函数开始吐帧,这里做一个简单的写文件逻辑。


AV_LOGI("==== onDataCallback ====, numFrames:%d", numFrames);
auto recorder = reinterpret_cast<SVAAudioRecorder *>(userData);

int32_t data_len = AAudioStream_getChannelCount(stream) * numFrames * 2;
if(recorder->file_) {
  size_t write_len = fwrite(audioData, 1, data_len, recorder->file_);
  AV_LOGI("Write into file data len:%d", write_len);
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;


停止采集并销毁


停止采集相关逻辑:


if(!initialized_ || !recording_) {
  AV_LOGW("StopRecording error, Invalid state.");
  return SV_STATE_ERROR;
}

aaudio_result_t result = AAudioStream_requestStop(stream_);
if (result != AAUDIO_OK) {
  AV_LOGW("StopRecording error: %d, reason:%s", result, AAudio_convertResultToText(result));
  return SV_STOP_ERROR;
}
recording_ = false;
initialized_ = false;


销毁


AAudioStream_close(stream_);
stream_ = nullptr;
AAudioStreamBuilder_delete(builder_);


/   Oboe   /


在上面的章节说过,Oboe 的存在是为了解决 Opensl es 调用逻辑繁琐 和 AAudio 不能运行在 Android O 版本以下设备的尴尬局面。它的 API 使用和 AAudio 如出一辙,这里不再赘述,唯一需要注意的是,它是 google 开源的一个项目,并非是 ndk 对外支持的接口。所以需要通过 gradle 依赖集成到项目中。主要的项目配置如下:


build.gradle 配置(模块级别)


android {
  //...
   buildFeatures {
        prefab true
    }
}

dependencies {
    implementation 'com.google.oboe:oboe:1.9.0'
}


cmake 配置


find_package (oboe REQUIRED CONFIG)

target_link_libraries(${CMAKE_PROJECT_NAME}
        # List libraries link to the target library
        android
        log
        oboe::oboe)


头文件引入


#include <oboe/Oboe.h>


具体代码调用逻辑见文章最后的 samplecode 链接。


/   最后   /


看到最后可能还是会有很多读者对使用哪种音频采集感到困惑,笔者根据自己的经验来推荐下。


  • 如果仅仅是本地录制或者对音频采集延迟没什么要求的使用 Java AudioRecord,毕竟兼容性比较好,无论是手机还是 IOT 设备都是完美支持的。

  • 专业的音频处理场景,不仅包括音频采集,还有各种音频处理,例如音效、均衡处理等,建议使用 Opensl es。

  • 如果新的项目,或者面向的用户群体 Android 版本很高,建议使用 AAudio 或者 oboe 均可。

  • DAU 较大且对音频延迟要求较高的应用,建议 Opensl es 和 AAudio 混用,并自己完成音频采集回退机制。自己做的好处是通过埋点,可以比较两种方式的不同机型或Android 版本的线上故障率,另外线上用户出现紧急故障可以实现动态切换。


以上就是本文的所有内容了,介绍了 Android 平台下三种高性能音频采集的方式。


github samplecode: 

https://github.com/Sound-Vision/audio_record/tree/main/android


推荐阅读:

我的新书,《第一行代码 第3版》已出版!

那些大厂架构师是怎样封装网络请求的?

提升 WebView 用户体验的关键:Android WebChromeClient 解析


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注

继续滑动看下一个
郭霖
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存